package math;

public class mat4
{
    
    public static final int NUM_ROWS = 4;
    public static final int NUM_COLS = 4;
    
    public static final int NUM_ENTRIES = mat4.NUM_ROWS * mat4.NUM_COLS;
    
    public static final mat4 IDENTITY = new mat4();
    
    public static final mat4 ZERO = new mat4(0.0);
    
    
    /**
    Array containing matrix entries. Index-form:
        0   4   8   12  
        1   5   9   13  
        2   6   10  14  
        3   7   11  15
    */
    public double[] m;
    
    
    public mat4()
    {
        this(1.0);
    }
    
    public mat4(double aEntry)
    {
        this.m = new double[mat4.NUM_ENTRIES];
        this.m[0] = this.m[5] = this.m[10] = this.m[15] = aEntry;
    }
    
    public mat4(mat3 aOther)
    {
        this(1.0);
        this.set(aOther);
    }
    
    
    public final void set(mat4 aOther)
    {
        System.arraycopy(aOther.m, 0, this.m, 0, mat4.NUM_ENTRIES);
    }
    
    public final void set(mat3 aOther)
    {
        System.arraycopy(aOther.m, 0, this.m, 0, mat3.NUM_ROWS); // 1st column
        System.arraycopy(aOther.m, 3, this.m, 4, mat3.NUM_ROWS); // 2nd column
        System.arraycopy(aOther.m, 6, this.m, 8, mat3.NUM_ROWS); // 3rd column
    }
    
    
    public void mul(mat4 aOther)
    {
        mat4 mul = mat4.mul(this, aOther);
        this.set(mul);
    }
    
    public void mulEntrywise(mat3 aOther)
    {
        for (int i = 0; i < mat3.NUM_ENTRIES; i++)
        {
            double ithMultiply = this.m[i] * aOther.m[i];
            this.m[i] = ithMultiply;
        }
    }
    
    public void transpose()
    {
        mat4.swap(this, 1, 4); // Swap 1 <-> 4.
        mat4.swap(this, 2, 8); // Swap 2 <-> 8.
        mat4.swap(this, 3, 12); // Swap 3 <-> 12.
        mat4.swap(this, 6, 9); // Swap 6 <-> 9.
        mat4.swap(this, 7, 13); // Swap 7 <-> 13.
        mat4.swap(this, 11, 14); // Swap 11 <-> 14.
    }
    
    
    public vec4 mul(vec4 aVec)
    {
        return new vec4(
            this.m[0] * aVec.x + this.m[4] * aVec.y + this.m[8]  * aVec.z + this.m[12] * aVec.w, 
            this.m[1] * aVec.x + this.m[5] * aVec.y + this.m[9]  * aVec.z + this.m[13] * aVec.w, 
            this.m[2] * aVec.x + this.m[6] * aVec.y + this.m[10] * aVec.z + this.m[14] * aVec.w, 
            this.m[3] * aVec.x + this.m[7] * aVec.y + this.m[11] * aVec.z + this.m[15] * aVec.w
        );
    }
    
    
    public mat4 copy()
    {
        mat4 copy = new mat4();
        copy.set(this);
        return copy;
    }
    
    
    public String toString()
    {
        StringBuilder sb = new StringBuilder();
        sb.append("mat4");
        for (int j = 0; j < mat4.NUM_ROWS; j++)
        {
            sb.append("\n   ");
            for (int i = 0; i < mat4.NUM_COLS; i++)
            {
                int ind = 4 * i + j;
                sb.append(String.format("%.2f", this.m[ind]));
                if (i < mat4.NUM_COLS - 1)
                {
                    sb.append(", ");
                }
            }
        }
        return sb.toString();
    }
    
    
    public static mat4 mul(mat4 aA, mat4 aB)
    {
        mat4 mul = new mat4();
        mat4.mul(aA, aB, mul);
        return mul;
    }
    
    public static mat4 rotate(vec3 aAxis, double aAngleRad)
    {
        mat4 rot = new mat4(mat3.rotate(aAxis, aAngleRad));
        return rot;
    }
    
    public static mat4 rotateX(double aAngleRad)
    {
        mat4 rot = new mat4(mat3.rotateX(aAngleRad));
        return rot;
    }
    
    public static mat4 rotateY(double aAngleRad)
    {
        mat4 rot = new mat4(mat3.rotateY(aAngleRad));
        return rot;
    }
    
    public static mat4 rotateZ(double aAngleRad)
    {
        mat4 rot = new mat4(mat3.rotateZ(aAngleRad));
        return rot;
    }
    
    public static mat4 rotateZYX(vec3 aAnglesRad)
    {
        mat4 rzyx = new mat4(mat3.rotateZYX(aAnglesRad));
        return rzyx;
    }
    
    public static mat4 rotateXYZ(vec3 aAnglesRad)
    {
        mat4 rxyz = new mat4(mat3.rotateXYZ(aAnglesRad));
        return rxyz;
    }
    
    public static mat4 scale(vec4 aScale)
    {
        mat4 scale = new mat4();
        scale.m[0] = aScale.x;
        scale.m[5] = aScale.y;
        scale.m[10] = aScale.z;
        scale.m[15] = aScale.w;
        return scale;
    }
    
    public static mat4 transl(vec3 aTransl)
    {
        mat4 transl = new mat4();
        transl.m[12] = aTransl.x;
        transl.m[13] = aTransl.y;
        transl.m[14] = aTransl.z;
        return transl;
    }
    
    public static mat4 ortho(
            vec2 aSpanX, 
            vec2 aSpanY, 
            vec2 aSpanZ)
    {
        mat4 ortho = new mat4();
        ortho.m[0] = 2.0 / (aSpanX.y - aSpanX.x);
        ortho.m[5] = 2.0 / (aSpanY.y - aSpanY.x);
        ortho.m[10] = -2.0 / (aSpanZ.y - aSpanZ.x);
        ortho.m[12] = (aSpanX.x + aSpanX.y) / (aSpanX.x - aSpanX.y);
        ortho.m[13] = (aSpanY.x + aSpanY.y) / (aSpanY.x - aSpanY.y);
        ortho.m[14] = (aSpanZ.x + aSpanZ.y) / (aSpanZ.x - aSpanZ.y);
        return ortho;
    }
    
    public static mat4 perspective(
            vec2 aClipZ, 
            double aAspectRatio, 
            double aVertFOV)
    {
        vec2 spanY = new vec2(math.tan(0.5 * aVertFOV) * aClipZ.x);
        spanY.x *= -1.0;
        vec2 spanX = vec2.mul(spanY, aAspectRatio);
        return mat4.perspective(spanX, spanY, aClipZ);
    }
    
    public static mat4 perspective(
            vec2 aSpanX, 
            vec2 aSpanY, 
            vec2 aSpanZ)
    {
        mat4 perspec = new mat4();
        double rec_SpanX = 1.0 / (aSpanX.y - aSpanX.x);
        double rec_SpanY = 1.0 / (aSpanY.y - aSpanY.x);
        double rec_SpanZ = 1.0 / (aSpanZ.y - aSpanZ.x);
        
        perspec.m[0] = 2.0 * aSpanZ.x * rec_SpanX; // 2 * n / (r - l)
        perspec.m[5] = 2.0 * aSpanZ.x * rec_SpanY; // 2 * n / (t - b)
        perspec.m[8] = (aSpanX.x + aSpanX.y) * rec_SpanX; // (r + l) / (r - l)
        perspec.m[9] = (aSpanY.x + aSpanY.y) * rec_SpanY; // (t + b) / (t - b)
        perspec.m[10] = -(aSpanZ.x + aSpanZ.y) * rec_SpanZ; // -(f + n) / (f - n)
        perspec.m[11] = -1.0;
        perspec.m[14] = -2.0 * aSpanZ.x * aSpanZ.y * rec_SpanZ; // -2 * n * f / (f - n)
        perspec.m[15] = 0.0;
        return perspec;
    }
    
    
    
    private static void mul(mat4 aA, mat4 aB, mat4 aDST)
    {
        for (int i = 0; i < mat4.NUM_ENTRIES; i++)
        {
            int imod4 = i % 4;
            int idiv4 = i / 4;
            double dotProduct = 0.0;
            for (int j = 0; j < 4; j++)
            {
                int indA = j * 4 + imod4;
                int indB = j + 4 * idiv4;
                double jthMultiply = aA.m[indA] * aB.m[indB];
                dotProduct += jthMultiply;
            }
            aDST.m[i] = dotProduct;
        }
    }
    
    private static void swap(mat4 aA, int indA, int indB)
    {
        double t = aA.m[indA];
        aA.m[indA] = aA.m[indB];
        aA.m[indB] = t;
    }
    
}
